Release 10.1A: OpenEdge Development:
Progress 4GL Handbook


Adding dynamic fields to the test window

The first thing you need to add to h-CustOrderWin7.w to create dynamic fields is a variable or other storage to hold their handles. Because the whole purpose of the exercise is to allow the user to select a variable number of fields to display, there is no reasonable way to store each one’s handle in a separate variable.

Storing a list of handles

You could store the object handles in a HANDLE variable array that has an EXTENT, but this is almost certainly a bad idea. The first rule of using a variable with an extent is that you should do it only when the proper value for the extent is clear, based on the nature of the data it is holding, such as values for the seven days in a week or the twelve months in a year. If you just try to pick a value that seems big enough, you will often regret it later when that number turns out to be too small for some case you hadn’t anticipated.

The method used in the example is just to store the handles in a list, in character form. For a modest number of values, this is quite reasonable, and the conversion effort back and forth between a handle and its character representation is not significant.

Always keep in mind the alternative of using a temp-table to store a set of values during program execution. Although the overhead of having to perform a FIND on what amounts to a special database table may seem significant, in fact temp-tables are extremely fast. Most or all of the records you need to work with will likely be in memory anyway and, with the ability to index fields that you need to retrieve or filter on, even a large temp-table should provide very good performance. A temp-table is well suited to situations where the number of possible values you need to keep track of can grow large. How large is large? There’s no precise answer to this, but it is probably a good rule of thumb that if you’re storing more than a few dozen values, it is cleaner and possibly faster to use a temp-table. A temp-table is also the right choice when you need to store several related pieces of information for each item, each of which can become a field in the temp-table definition.

For this example you simply use a character variable. Its value needs to persist for the life of the procedure, because the handles are saved off by one internal procedure or trigger block and used by another.

To create dynamic fields in the sample window:

  1. Define the cFieldHandles variable in the Definitions section of h-CustOrderWin7.w, which scopes the variable definition to the whole procedure:
  2. /* Local Variable Definitions ---                                       */ 
    DEFINE VARIABLE cFieldHandles AS CHARACTER  NO-UNDO. 
    

  3. Write a block of code to execute whenever a new Order is selected. This is the VALUE-CHANGED event for the browse, which you’ve used in an earlier variation of this procedure.
  4. The code in the VALUE-CHANGED trigger needs to find the first OrderLine for the Order. For the sake of simplicity, the example does not navigate through all the OrderLines, but you could easily extend it to do this. Then, it looks at the existing list of dynamic field handles (if any) and clears them out by setting their SCREEN-VALUE to blank:

    /* ON VALUE-CHANGED OF OrderBrowse */ 
    DO: 
      DEFINE VARIABLE iField AS INTEGER    NO-UNDO. 
      DEFINE VARIABLE hField AS HANDLE     NO-UNDO. 
      FIND FIRST OrderLine OF Order. 
      DO iField = 2 TO NUM-ENTRIES(cFieldHandles) BY 2: 
          hField = WIDGET-HANDLE(ENTRY(iField, cFieldHandles)). 
          IF VALID-HANDLE(hField) THEN 
              hField:SCREEN-VALUE = "". 
      END. 
    END. 
    

  5. Define a LEAVE trigger for the OLineFields selection list. The trigger uses these variables:
  6.   DEFINE VARIABLE iField    AS INTEGER    NO-UNDO. 
      DEFINE VARIABLE hField    AS HANDLE     NO-UNDO. 
      DEFINE VARIABLE hLabel    AS HANDLE     NO-UNDO. 
      DEFINE VARIABLE cFields   AS CHARACTER  NO-UNDO. 
      DEFINE VARIABLE cField    AS CHARACTER  NO-UNDO. 
      DEFINE VARIABLE hBufField AS HANDLE     NO-UNDO. 
      DEFINE VARIABLE dRow      AS DECIMAL    NO-UNDO INIT 8.0. 
    

  7. To allow for the case where this is not the first time the user has selected a list of fields, add code that first deletes the existing fields using their object handles, which are stored in a list in the cFieldsHandle variable:
  8.   DO iField = 1 TO NUM-ENTRIES(cFieldHandles): 
          hField = WIDGET-HANDLE(ENTRY(iField,cFieldHandles)). 
          DELETE OBJECT hField NO-ERROR. 
      END. 
    

    Remember that if you neglect to do this, each new request would add more objects to the session that aren’t being used anymore. The NO-ERROR qualifier on the DELETE OBJECT statement simply suppresses any error message in the event that the object has already been deleted in some other way.

    How about when the procedure is terminated? Do you need code to delete the dynamic fields that are around at that time to prevent a memory leak? The answer is no, but only because of the widget pool created in the Definitions section, which cleans up all dynamic objects created by the procedure when the procedure terminates. That’s why the widget pool convention is so valuable. Without the widget pool created for the procedure, you could leave dynamic objects in memory for the duration of the session, even after the procedure exits.

    Since this code is the LEAVE trigger for the selection list, the field’s SCREEN-VALUE attribute holds the value the user selected. In the case of a multiple-selection list such as this, the value is actually a comma-separated list of all the entries the user selected.

  9. Save this value in a variable to keep the rest of the code from having to refer to the SCREEN-VALUE attribute over and over again:
  10. cFields = OLineFields:SCREEN-VALUE. 
    

  11. Add a block to iterate through all the selections. You saw earlier how the BUFFER-FIELD attribute on a buffer handle can take the ordinal position of the field in the buffer as an identifier. You can also pass the field name, as the code does here. Once you’ve retrieved the handle of the selected field, the code can query a number of different field attributes through that handle:
  12.   DO iField = 1 TO NUM-ENTRIES(cFields): 
          ASSIGN cField = ENTRY(iField, cFields) 
                 hBufField = BUFFER OrderLine:BUFFER-FIELD(cField). 
    

  13. Create the text label for the fill-in. As you learned earlier, the label must be a separate text object:
  14.   CREATE TEXT hLabel 
             ASSIGN FRAME = FRAME CustQuery:HANDLE 
                    DATA-TYPE = "CHARACTER" 
                    FORMAT = "X(" + STRING(LENGTH(hBufField:LABEL) + 1) + ")" 
                    SCREEN-VALUE = hBufField:LABEL + ":" 
                    HEIGHT-CHARS = 1 
                    ROW = dRow 
                    COLUMN = 85.0. 
    

    The CREATE statement parents it to the frame, sets its data type, calculates a format and value for it using the LABEL attribute of the current buffer field, and positions it in the frame. The HEIGHT-CHARS of 1 makes the label text align properly with the value displayed next to it. The COLUMN positions it next to the browse, and the row is incremented each time through the loop to define a distinct position for each field.

  15. Create the fill-in object itself:
  16. CREATE FILL-IN hField  
              ASSIGN DATA-TYPE = hBufField:DATA-TYPE 
                     FORMAT = hBufField:FORMAT 
                     FRAME = FRAME CustQuery:HANDLE 
                     SIDE-LABEL-HANDLE = hLabel 
                     COLUMN = 85.0 + LENGTH(hBufField:LABEL) + 4 
                     ROW = dRow 
                     SCREEN-VALUE = hBufField:BUFFER-VALUE 
                     HIDDEN = NO. 
    

    The data type, format, and value all come from the buffer field object handle. The SIDE-LABEL-HANDLE attribute connects this fill-in to its handle object. The COLUMN setting allows room for the label before displaying the field value. The SCREEN-VALUE assigns the value from the buffer field’s BUFFER-VALUE attribute. The HIDDEN attribute makes sure the field is viewed along with the frame that contains it.

  17. Increment the row counter to set the position of the next field, and save off the handles of the labels and fill-ins in a list:
  18. ASSIGN dRow = dRow + 1.0 
               cFieldHandles = cFieldHandles +  
                  (IF cFieldHandles = "" THEN "" ELSE ",") +  
                     STRING(hLabel) + "," + STRING(hField). 
    END.   /* END DO iField */ 
    

  19. Make sure that the VALUE-CHANGED trigger for the Order browse fires whenever a different record is displayed. This includes when the procedure first starts up, so make this addition to the main block:
  20. MAIN-BLOCK: 
    DO ON ERROR   UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK 
       ON END-KEY UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK: 
      RUN enable_UI. RUN h-StartSuper.p("h-dynsuper.p"). 
      RUN changeFields. 
      APPLY "VALUE-CHANGED" TO OrderBrowse. 
      RUN initSelection. 
      IF NOT THIS-PROCEDURE:PERSISTENT THEN 
        WAIT-FOR CLOSE OF THIS-PROCEDURE. 
    END. 
    

  21. Make the same addition to each of the navigation button triggers, as you have done to another version of the procedure in Chapter 7, " Record Buffers and Record Scope."
  22. Run the window. Now you can select one or more fields from the selection list, tab out of it, and see those fields displayed as dynamic fill-ins with dynamic labels next to the browse:
  23. If a few of the fields seem to be positioned rather far to the right (the Order Num for instance), it’s because they are right-justified numeric fields with overly generous display formats as defined in the Data Dictionary. Specifically, the OrderNum and ItemNum fields are defined in the schema with a long format that uses the Z character to format leading zeros. The Z tells Progress to replace leading zeroes with spaces, which pushes the displayed value out to the right. Others, such as the Price, are formatted with the > character, which tells Progress to suppress leading zeroes, effectively left-justifying the value. This is just a result of the formatting choices made by the database designer and has nothing to do with the display of dynamic values.

Using the FONT-TABLE to make the labels colon-aligned

This display looks all right as far as it goes, but in many cases you want your labels to appear right-justified rather than left-justified. In other words, you want the colons that end each label to be vertically aligned, so that all the field values can begin at the same column position to the right of that. How can you do this?

Progress provides a built-in system handle, called FONT-TABLE, which is an object representing the current font. There are four useful methods you can apply to this handle to calculate the actual size of a value when it’s displayed: GET-TEXT-WIDTH-CHARS, GET-TEXT-HEIGHT-CHARS, GET-TEXT-WIDTH-PIXELS, and GET-TEXT-HEIGHT-PIXELS. In an alternative version of the trigger code for the selection list, you can use this function to align the labels on their colons.

To colon-align your labels:

  1. Change the column setting in the CREATE TEXT statement for the label to this:
  2. /* COLUMN = 85.0.  -- modified to do colon-aligned labels */ 
    COLUMN = 100.0 - FONT-TABLE:GET-TEXT-WIDTH-CHARS(hBufField:LABEL + ":"). 
    

    Instead of starting at column 85 and positioning the label to the right, the statement starts where the labels should end (column position 100), and subtracts the label width as calculated by the method on the FONT-TABLE. This gives the right starting position for the label object.

  3. Change the column assignment for the fill-in to be fixed at column 102:
  4. /* COLUMN = 85.0 + LENGTH(hBufField:LABEL) + 4  -- changed for 
    colon-aligned */ 
    COLUMN = 102 
    

    This places each displayed value at the same position, two positions to the right of the label.

    There’s a third change you have to make as well. As discussed in the "Object sizing" section, the format calculation for the label is likely to provide a format somewhat larger than the actual display width. This is a deliberate adjustment Progress makes to provide a format for a field that is large enough to display most values without truncation. In the case of your labels, however, you might find that if you just use the label format to determine the width, the display width is a bit too large and overwrites the beginning of some of the displayed values with blanks. To correct this, you need to specify an accurate WIDTH-CHARS attribute value for the label, so that it is just large enough to display itself without truncation but not so large that it overwrites the value that follows it.

  5. Use the same FONT-TABLE method to calculate the WIDTH-CHARS of the text label object, adding this assignment to the CREATE TEXT statement:
  6. WIDTH-CHARS = FONT-TABLE:GET-TEXT-WIDTH-CHARS(hBufField:LABEL + ":") 
    

  7. Run the window. With these changes, you see a different display where the labels of the dynamic fields are colon-aligned:
  8. Select another row in the browse, and the field values are set to blank by the VALUE-CHANGED trigger on the browse. What you see, however, is that because the displayed fields are not all CHARACTER fields, Progress applies the field’s format to the blank value, which in the case of a numeric field is the value zero, and is displayed in various ways by the different field formats:
  9. Clearly this procedure is an interesting example of using dynamic fields but not a terribly useful application window. You would probably want to be able to select a list of fields once and use them to display OrderLine values for any Order you select, rather than having the fields blanked out after one OrderLine is displayed. And there must be a way to navigate through the OrderLines of an Order rather than just seeing the first one. Feel free to extend the test procedure to provide these abilities.

    Since there’s room in the window to display all the OrderLine fields, the whole notion of making them dynamic fill-ins is also of limited use. A more realistic example of how you can use this technique in an application would be for a table with a great many fields, only a few of which each user needs to work with. After you learn about dynamic data management objects in the next chapter, you will also be able to allow the user to select a completely different table to display in place of the OrderLines.

Field format versus width

The relationship between format and width that the example highlights might seem confusing. Keep these basic guidelines in mind:


Copyright © 2005 Progress Software Corporation
www.progress.com
Voice: (781) 280-4000
Fax: (781) 280-4095